<!--
  自定义动态表单，根据配置动态生产表单字段

  1. 使用方式：
    <dynamic-form ref="dynamicFormRef" v-model="form" label-width="100px" :label-position="labelPosition" :columns="fieldColumns" @change="onDynamicFormChange"></dynamic-form>

  2. 字段配置实例：
    [
      {
          fieldType: 'input',
          fieldName: 'username',
          label: '用户名',
          col: {
            span: 8,
          },
          props: {
            placeholder: '请输入用户名',
          },
          validate: [
            { required: true, message: '请输入用户名', trigger: 'blur' },
          ],
        },
        {
          fieldType: 'radio',
          fieldName: 'other.sex',
          label: '性别',
          col: {
            span: 8,
          },
          props: {
            clearable: true,
          },
          options: [{ value: 1, label: '男' }, { value: 2, label: '女' }, { value: -1, label: '未知' }],
          validate: [{ required: true, message: '请选择', trigger: 'change' }],
        },
        {
          fieldType: 'select-dictionary',
          fieldName: 'belongFieldCode',
          label: '所属领域',
          col: {
            span: 8,
          },
          props: {
            job: '所属领域',
            clearable: true,
          },
          validate: [{ required: true, message: '请选择', trigger: 'change' }],
        },
        {
          fieldType: 'select-dictionary',
          fieldName: 'categoryCode',
          label: '所属类别',
          col: {
            span: 8,
          },
          props: {
            pid: 'belongFieldCode',
            clearable: true,
          },
          validate: [{ required: true, message: '请选择', trigger: 'change' }],
        },
    ]
-->

<template>
  <el-form ref="dynamicFormRef" class="dynamic-form" :label-width="labelWidth" :label-position="labelPosition" :model="dynamicFormValue" :disabled="disabled">
    <el-row v-for="(row, rowIdx) in rowGroups" :key="rowIdx">
      <el-col v-for="(column, index) in row" :key="index" :span="column.col.span">
        <el-form-item
          v-if="column.visible ? column.visible(dynamicFormValue) : true"
          :label="column.label"
          :label-width="column.col.labelWidth"
          :prop="column.fieldName"
          :rules="column.validate"
        >
          <form-item-middleware
            :value="dynamicFormValue[column.fieldName]"
            :data-source="dynamicFormValue"
            :field-name="column.fieldName"
            :form-attribute="column"
            @change="_handleChange"
          ></form-item-middleware>
        </el-form-item>
      </el-col>
    </el-row>
  </el-form>
</template>

<script>
import _ from 'lodash';
import FormItemMiddleware from './form-item-middleware';

export default {
  name: 'dynamic-form',
  components: { FormItemMiddleware },
  model: {
    prop: 'formValue',
    event: 'change',
  },
  props: {
    /**
     * 表单绑定的对象
     */
    formValue: {
      type: Object,
      required: true,
    },
    /**
     * 字段
     *  {
     *    fieldType:  字段类型
     *    fieldName:  字段名称
     *    label:      字段 label
     *    visible:  { function(form) } 用来控制当前字段的显示
     *    col:        表单字段布局
     *       {
     *         span   栅格占据的列数
     *         labelWidth 表单域标签的的宽度
     *       }
     *    props:      基础组件 props; { clearable: true, placeholder: '请输入', ... }
     *    options:    选择项; [{ value: 1, label: '标签1' }, { value: 2, label: '标签2' }, ...]
     *    optionValue: 列表中 value 对应的字段 key, 不设置默认为 value
     *    optionLabel: 列表中 label 对应的字段 key, 不设置默认为 label
     *    validate:   字段校验规则，可参看 element-ui 表单组件的校验规则
     *      [{ required: true, message: '请输入内容', trigger: 'blur' }]
     *    on: { function{obj} } 监听值变化的回调函数 - obj: { $el, record, form, column }
     *    onSelectItem: { function{obj} } 监听选择返回源数据对象的回调函数  - obj: { $el, record, form, column }
     *  }
     */
    columns: {
      type: Array,
      required: true,
    },
    /**
     * 是否禁用该表单内的所有组件。若设置为 true，则表单内组件上的 disabled 属性不再生效
     */
    disabled: {
      type: Boolean,
      default: false,
    },
    /**
     * 表单域标签的宽度，例如 '50px'。作为 Form 直接子元素的 form-item 会继承该值。支持 auto。
     */
    labelWidth: {
      type: String,
    },
    /**
     * 表单域标签的位置，如果值为 left 或者 right 时，则需要设置 label-width
     */
    labelPosition: {
      type: String,
      default: 'right', // right | left | top
    },
    /**
     * 用于控制该表单内组件的尺寸: medium / small / mini
     */
    size: {
      type: String,
    },
  },
  data() {
    return {
      dynamicFormValue: {}, // 动态表单数据对象
      rowGroups: [],
    };
  },

  watch: {
    formValue: {
      handler(val) {
        this.dynamicFormValue = _.cloneDeep(val);
      },
      immediate: true,
      deep: true,
    },
    columns: {
      handler(list) {
        if (list && list.length > 0) {
          const lastIndex = list.length - 1;
          let rows = [];

          list.forEach((column, idx) => {
            // 设置字段布局的默认值
            if (!column.col) {
              column.col = { span: 24 };
            } else if (!column.col.span) {
              column.col.span = 24;
            }

            // 设置表单组件默认配置
            this._setDefaultProps(column);
            // 设置布局, 将表单字段按照设置的 col.span 的值进行分组
            const totalSpan = _.reduce(rows, (sum, item) => (sum + item.col.span), 0);
            if (totalSpan + column.col.span < 24) {
              rows.push(column);
              if (idx === lastIndex) {
                this.rowGroups.push(rows);
              }
            } else if (totalSpan + column.col.span === 24) {
              this.rowGroups.push([...rows, column]);
              rows = [];
            } else {
              this.rowGroups.push(rows);
              rows = [column];
              if (idx === lastIndex) {
                this.rowGroups.push(rows);
              }
            }

            // 初始化值
            this._initDynamicFormValue(column);
          });
          console.log('[dynamic-form created]', JSON.parse(JSON.stringify(this.dynamicFormValue)));
        }
      },
      immediate: true,
      deep: true,
    },
  },

  methods: {
    /**
     * 设置默认的 props 配置
     */
    _setDefaultProps(column) {
      if (!column.props) {
        column.props = { clearable: true };
      } else if (_.isUndefined(column.props.clearable)) {
        column.props.clearable = true;
      }
    },

    /**
     * 初始化表单属性字段的值，如果有路径属性('a.b')的模式，可以将其转化为对象(obj: { a: { b: null } })
     */
    _initDynamicFormValue(column) {
      const defaultValue = _.get(this.dynamicFormValue, column.fieldName);
      let value = null;
      if (['select', 'select-dictionary'].includes(column.fieldType)) {
        value = column.props && column.props.multiple ? [] : null;
      } else if (['checkbox-group'].includes(column.fieldType)) {
        value = [];
      }
      _.set(this.dynamicFormValue, column.fieldName, defaultValue || value);
    },

    /**
     * 处理字段值改变的回调函数
     */
    _handleChange(val, form, column) {
      _.set(this.dynamicFormValue, column.fieldName, val);
      this.$emit('change', this.dynamicFormValue, form, column);
    },

    /**
     * 对整个表单进行校验的方法，参数为一个回调函数。该回调函数会在校验结束后被调用，并传入两个参数：是否校验成功和未通过校验的字段。若不传入回调函数，则会返回一个 promise
     * @param callback { function(boolean, object) }
     */
    validate(callback) {
      this.$refs['dynamicFormRef'].validate(callback);
    },

    /**
     * 对部分表单字段进行校验的方法
     * @param props { array | string }
     * @param callback { function(errorMessage: string) }
     */
    validateField(props, callback) {
      this.$refs['dynamicFormRef'].validateField(props, callback);
    },

    /**
     * 重置表单
     */
    resetFields() {
      this.$refs['dynamicFormRef'].resetFields();
    },

    /**
     * 移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组，如不传则移除整个表单的校验结果
     * @param props { array | string }
     */
    clearValidate(props) {
      this.$refs['dynamicFormRef'].clearValidate(props);
    },

    /**
     * 清除表单所有字段，表单字段(DOM)重新构建
     */
    clearForm() {
      if (this.$refs['dynamicFormRef'] && this.$refs['dynamicFormRef'].$el.hasChildNodes()) {
        this.$refs['dynamicFormRef'].$el.innerHTML = '';
      }
    },
  },
};
</script>

<style lang="scss" scoped>
.dynamic-form {
  width: 100%;

  &.el-form--label-top {
    /deep/ .el-col {
      padding: 0 12px;
      margin: 0 -6px;

      .el-form-item__label {
        padding: 0;
      }
    }
  }

  &.el-form--label-left {
    /deep/ .el-col {
      padding-left: 12px;
      margin-left: -6px;
    }
  }
}
</style>
